﻿using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.WowAddonStudio.Lua.Loaders;

namespace Microsoft.WowAddonStudio.Lua.LanguageService.Intellisense.Providers
{
    /// <summary>
    /// Provides the streams to the TOC file and other files based on a Visual Studio project
    /// hierarchy.
    /// </summary>
    public class HierarchyStreamProvider : StreamProvider
    {
        private const string tocExtension = ".toc";

        private readonly IServiceProvider serviceProvider;
        private readonly IVsHierarchy hierarchy;

        /// <summary>
        /// Initializes a new instance of the <see cref="HierarchyStreamProvider"/> class.
        /// </summary>
        /// <param name="serviceProvider"></param>
        /// <param name="hierarchy">The Visual Studio hierarchy.</param>
        public HierarchyStreamProvider(IServiceProvider serviceProvider, IVsHierarchy hierarchy)
        {
            if (serviceProvider == null) 
                throw new ArgumentNullException("serviceProvider");
            if (hierarchy == null)
                throw new ArgumentNullException("hierarchy");

            this.serviceProvider = serviceProvider;
            this.hierarchy = hierarchy;
        }

        /// <summary>
        /// Gets the canonical name of the root item.
        /// </summary>
        public string RootName
        {
            get
            {
                return this.GetCanonicalName(VSConstants.VSITEMID_ROOT);
            }
        }

        /// <summary>
        /// Gets the <see cref="Stream"/> to a World of Warcraft TOC file.
        /// </summary>
        /// <returns>An instance of the <see cref="Stream"/> class.</returns>
        public override Stream GetTocStream()
        {
            string tocCanonicalName = Path.Combine(Path.GetDirectoryName(this.RootName), Path.GetFileNameWithoutExtension(this.RootName) + tocExtension);
            if (tocCanonicalName != null)
            {
                // Find the itemid of the TOC file
                uint? tocItemid = this.GetCanonicalNameItemId(tocCanonicalName);

                // If the TOC item was found by its canonical name, return stream
                if (tocItemid.HasValue)
                    return this.GetStream(tocItemid.Value);
            }

            return null;
        }

        /// <summary>
        /// Gets a <see cref="Stream"/> to a file.
        /// </summary>
        /// <param name="filePath">The path of the file.</param>
        /// <returns>An instance of the <see cref="Stream"/> class.</returns>
        public override Stream GetFileStream(string filePath)
        {
            if (filePath == null)
                throw new ArgumentNullException("filePath");

            string fileCanonicalName = Path.Combine(Path.GetDirectoryName(this.RootName), filePath);
            if (fileCanonicalName != null)
            {
                // Find the itemid of the file
                uint? fileItemid = this.GetCanonicalNameItemId(fileCanonicalName);

                // If the item was found by its canonical name, return stream
                if (fileItemid.HasValue)
                    return this.GetStream(fileItemid.Value);
            }

            return null;
        }

        private Stream GetStream(uint itemid)
        {
            string documentMoniker = this.GetCanonicalName(itemid);

            var rdt = (IVsRunningDocumentTable)serviceProvider.GetService(typeof(SVsRunningDocumentTable));
            if (rdt != null)
            {
                IVsHierarchy projectHierarchy;
                uint documentItemId;
                IntPtr docData;
                uint dwCookie;
                rdt.FindAndLockDocument((uint) _VSRDTFLAGS.RDT_NoLock, documentMoniker, out projectHierarchy,
                                        out documentItemId, out docData, out dwCookie);

                if (docData != IntPtr.Zero)
                {
                    var textLines = Marshal.GetObjectForIUnknown(docData) as IVsTextLines;
                    if (textLines != null)
                    {
                        int line, index;
                        string text;
                        textLines.GetLastLineIndex(out line, out index);
                        textLines.GetLineText(0, 0, line, index, out text);

                        return new MemoryStream(Encoding.Default.GetBytes(text));
                    }
                }
            }

            if (File.Exists(documentMoniker))
                return File.Open(documentMoniker, FileMode.Open, FileAccess.Read);

            return null;
        }

        private uint? GetCanonicalNameItemId(string canonicalName)
        {
            uint itemid;
            if (hierarchy.ParseCanonicalName(canonicalName, out itemid) == VSConstants.S_OK)
                return itemid;

            return VSConstants.VSITEMID_NIL;
        }

        private uint? GetRelativePathItemId(uint parentItemid, string path)
        {
            string[] pathElements = path.Split(new[]
                                      {
                                          Path.DirectorySeparatorChar
                                      }, StringSplitOptions.RemoveEmptyEntries);

            uint? itemid = parentItemid;

            foreach (var pathElement in pathElements)
            {
                if (!itemid.HasValue)
                    break;

                itemid = this.GetChildItemId(itemid.Value, pathElement);
            }

            return itemid;
        }

        private uint? GetChildItemId(uint itemid, string childName)
        {
            // Get the first child of the item
            uint childItemId;
            if (this.TryGetItemId(itemid, __VSHPROPID.VSHPROPID_FirstChild, out childItemId))
            {
                while (childItemId != VSConstants.VSITEMID_NIL)
                {
                    string name = this.GetCanonicalName(childItemId);
                    if (Path.GetFileName(name) == childName)
                        return childItemId;

                    if (!this.TryGetItemId(itemid, __VSHPROPID.VSHPROPID_NextSibling, out childItemId))
                        childItemId = VSConstants.VSITEMID_NIL;
                }
            }

            return null;
        }

        private string GetCanonicalName(uint itemid)
        {
            string name;
            hierarchy.GetCanonicalName(itemid, out name);
            return name;
        }

        private bool TryGetItemId(uint itemid, __VSHPROPID propid, out uint value)
        {
            value = VSConstants.VSITEMID_NIL;

            object itemidValue;
            if (!this.TryGetProperty(itemid, propid, out itemidValue))
                return false;

            if (itemidValue is int)
                value = (uint)(int)itemidValue;
            if (itemidValue is uint)
                value = (uint)itemidValue;
            if (itemidValue is short)
                value = (uint)(short)itemidValue;
            if (itemidValue is ushort)
                value = (ushort)itemidValue;
            if (itemidValue is long)
                value = (uint)(long)itemidValue;

            return true;
        }

        private bool TryGetProperty<T>(uint itemid, __VSHPROPID propid, out T value)
        {
            value = default(T);

            object val;
            if (hierarchy.GetProperty(itemid, (int)propid, out val) != VSConstants.S_OK)
                return false;

            value = (T)val;
            return true;
        }
    }
}
